﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.IO.Compression;

namespace EffiProz.Core.Lib.Tar
{
    /**
  * Note that this class <b>is not</b> a java.io.FileOutputStream,
  * because our goal is to greatly restrict the public methods of
  * FileOutputStream, yet we must use public methods of the underlying
  * FileOutputStream internally.  Can't accomplish these goals in Java if we
  * subclass.
  * <P>
  * This class is ignorant about Tar header fields, attributes and such.
  * It is concerned with reading and writing blocks of data in conformance with
  * Tar formatting, in a way convenient to those who want to write the header
  * and data blocks.
  * <P>
  * Users write file data by means of populating the provided, public byte array,
  * then calling the single write(int) method to write a portion of that array.
  * This design purposefully goes with efficiency, simplicity, and performance
  * over Java convention, which would not use public fields.
  * <P>
  * At this time, we do not support appending.  That would greatly decrease the
  * generality and simplicity of the our design, since appending is trivial
  * without compression and very difficult with compression.
  * <P>
  * Users must finish tar file creation by using the finish() method.
  * Just like a normal OutputStream, if processing is aborted for any reason,
  * the close() method must be used to free up system resources.
  * <P>
  * <B>SECURITY NOTE</B>
  * Due to pitiful lack of support for file security in Java before version 1.6,
  * this class will only explicitly set permissions if it is compiled for Java
  * 1.6.  If it was not, and if your tar entries contain private data in files
  * with 0400 or similar, be aware that that file can be pretty much be
  * extracted by anybody with access to the tar file.
  *
  * @see #finish
  * @see #close
  */
    public class TarFileOutputStream
    {

        public static class Compression
        {

            public const int NO_COMPRESSION = 0;
            public const int GZIP_COMPRESSION = 1;
            public const int DEFAULT_COMPRESSION = NO_COMPRESSION;
            public const int DEFAULT_BLOCKS_PER_RECORD = 20;
        }

        public static bool debug = false;
        protected int blocksPerRecord;
        protected long bytesWritten = 0;
        private Stream writeStream;
        private FileInfo targetFile;
        private FileInfo writeFile;

        /* This is not a "Writer", but the byte "Stream" that we write() to. */
        public byte[] writeBuffer;
        /* We purposefully provide no public getter or setter for writeBuffer.
         * No getter because the whole point of this class is that the byte
         * array is a direct, optimally efficient byte array.  No setter because
         * the inside implementation of this class is intimately dependent upon
         * the nature of the write buffer. */
        public static byte[] ZERO_BLOCK = new byte[512];

        /**
         * Convenience wrapper to use default blocksPerRecord and compressionType.
         *
         * @see #TarFileOutputStream(File, int, int)
         */
        public TarFileOutputStream(FileInfo targetFile)
            : this(targetFile, Compression.DEFAULT_COMPRESSION)
        {

        }

        /**
         * Convenience wrapper to use default blocksPerRecord.
         *
         * @see #TarFileOutputStream(File, int, int)
         */
        public TarFileOutputStream(FileInfo targetFile,
                                   int compressionType)
            : this(targetFile, compressionType,
                TarFileOutputStream.Compression.DEFAULT_BLOCKS_PER_RECORD)
        {

        }

        /**
         * This class does no validation or enforcement of file naming conventions.
         * If desired, the caller should enforce extensions like "tar" and
         * "tar.gz" (and that they match the specified compression type).
         * <P/>
         * It also overwrites files without warning (just like FileOutputStream).
         */
        public TarFileOutputStream(FileInfo targetFile, int compressionType,
                                   int blocksPerRecord)
        {

            this.blocksPerRecord = blocksPerRecord;
            this.targetFile = targetFile;
            writeFile = new FileInfo(targetFile.Directory.FullName + Path.PathSeparator +
                                 targetFile.Name + "-partial");

            if (this.writeFile.Exists)
            {
                throw new IOException(RB.singleton.getString(RB.MOVE_WORK_FILE,
                        writeFile.FullName));
            }

            //if (targetFile.Exists && !targetFile.canWrite()) {
            //    throw new IOException(RB.singleton.getString(RB.CANT_OVERWRITE,
            //            targetFile.getAbsolutePath()));
            //}

            DirectoryInfo parentDir = targetFile.Directory;

            if (!parentDir.Exists)
            {

                throw new IOException(RB.singleton.getString(RB.NO_PARENT_DIR,
                        parentDir.FullName));
            }

            writeBuffer = new byte[blocksPerRecord * 512];

            switch (compressionType)
            {

                case TarFileOutputStream.Compression.NO_COMPRESSION:
                    writeStream = writeFile.OpenWrite();
                    break;

                case TarFileOutputStream.Compression.GZIP_COMPRESSION:
                    writeStream =
                        new GZipStream(writeFile.OpenWrite(), CompressionMode.Compress);
                    break;

                default:
                    throw new ArgumentException(
                        RB.singleton.getString(
                            RB.COMPRESSION_UNKNOWN, compressionType));
            }

        }

        /**
         * This class and subclasses should write to the underlying writeStream
         * <b>ONLY WITH THIS METHOD</b>.
         * That way we can be confident that bytesWritten will always be accurate.
         */
        public void write(byte[] byteArray, int byteCount)
        {

            writeStream.Write(byteArray, 0, byteCount);

            bytesWritten += byteCount;
        }

        /**
         * The normal way to write file data (as opposed to header data or padding)
         * using this class.
         */
        public void write(int byteCount)
        {
            write(writeBuffer, byteCount);
        }

        /**
         * Write a user-specified 512-byte block.
         *
         * For efficiency, write(int) should be used when writing file body content.
         *
         * @see #write(int)
         */
        public void writeBlock(byte[] block)
        {

            if (block.Length != 512)
            {
                throw new ArgumentException(
                    RB.singleton.getString(RB.BAD_BLOCK_WRITE_LEN, block.Length));
            }

            write(block, block.Length);
        }

        /**
         * Writes the specified quantity of zero'd blocks.
         */
        public void writePadBlocks(int blockCount)
        {

            for (int i = 0; i < blockCount; i++)
            {
                write(ZERO_BLOCK, ZERO_BLOCK.Length);
            }
        }

        /**
         * Writes a single zero'd block.
         */
        public void writePadBlock()
        {
            writePadBlocks(1);
        }

        public int bytesLeftInBlock()
        {

            int modulus = (int)(bytesWritten % 512L);

            if (modulus == 0)
            {
                return 0;
            }

            return 512 - modulus;
        }

        /**
         * @throws IllegalStateException if end of file not on a block boundary.
         */
        public void assertAtBlockBoundary()
        {

            if (bytesLeftInBlock() != 0)
            {
                throw new ArgumentException(
                    RB.singleton.getString(
                        RB.ILLEGAL_BLOCK_BOUNDARY, bytesWritten.ToString()));
            }
        }

        /**
         * Rounds out the current block to the next block bondary.
         * If we are currently at a block boundary, nothing is done.
         */
        public void padCurrentBlock()
        {

            int padBytes = bytesLeftInBlock();

            if (padBytes == 0)
            {
                return;
            }

            write(ZERO_BLOCK, padBytes);

            // REMOVE THIS DEV-ASSERTION:
            assertAtBlockBoundary();
        }

        /**
         * Implements java.io.Flushable.
         *
         * @see java.io.Flushable
         */
        public void flush()
        {
            writeStream.Flush();
        }

        /**
         * Implements java.io.Closeable.
         * <P/>
         * <B>IMPORTANT:<B/>  This method <B>deletes</B> the work file after
         * closing it!
         *
         * @see java.io.Closeable
         */
        public void close()
        {

            writeStream.Close();
            writeFile.Delete();

        }

        public long getBytesWritten()
        {
            return bytesWritten;
        }

        /**
         * (Only) when this method returns successfully, the generated file will be
         * a valid tar file.
         *
         * This method always performs a close, so you never need to call the close
         * if your code makes it to this method.
         * (You do need to call close if processing is aborted before calling
         * finish()).
         *
         * @see #close
         */
        public void finish()
        {

            try
            {
                long finalBlock = bytesWritten / 512 + 2;

                if (finalBlock % blocksPerRecord != 0)
                {

                    // Round up total archive size to a blocksPerRecord multiple
                    finalBlock = (finalBlock / blocksPerRecord + 1)
                                 * blocksPerRecord;
                }

                int finalPadBlocks = (int)(finalBlock - bytesWritten / 512L);

                if (TarFileOutputStream.debug)
                {
                    Console.Out.WriteLine(RB.singleton.getString(RB.PAD_BLOCK_WRITE,
                            finalPadBlocks));
                }

                writePadBlocks(finalPadBlocks);
            }
            catch (IOException ioe)
            {
                try
                {
                    close();
                }
                catch (IOException )
                {

                    // Too difficult to report every single error.
                    // More important that the user know about the original Exc.
                }

                throw ioe;
            }

            writeStream.Close();
            writeFile.MoveTo(targetFile.FullName);
        }
    }
}
